#include "CWaveOut.h"

void CWaveOut_Init(CWaveOut* thiz)
{
	thiz->hWaveOut       = NULL;
	thiz->buf_write      = 0;
	thiz->buf_write_pos  = 0;
	thiz->full_buffers   = 0;
	thiz->buffered_bytes = 0;
	thiz->buffer_size    = BUFFER_SIZE;
}

void CWaveOut_Done(CWaveOut* thiz)
{
	if (thiz->hWaveOut)
		CWaveOut_Close(thiz);
}

static void CALLBACK waveOutProc(
			HWAVEOUT	hWaveOut,
			UINT		uMsg,
			DWORD		dwInstance,
			DWORD		dwParam1,
			DWORD		dwParam2)
{
	CWaveOut* thiz;
	LPWAVEHDR pHeader = (LPWAVEHDR)dwParam1;

	if (uMsg != WOM_DONE)
		return;

	thiz = (CWaveOut*)dwInstance;

	thiz->buffered_bytes -= pHeader->dwBufferLength;
}

int CWaveOut_Open(CWaveOut* thiz, int sample_rate, int channels)
{
	WAVEFORMATEX	wf;
	MMRESULT		result;
	DWORD			totalBufferSize;
	int				i;
	char*			buffer;

	int sample_size = 16;

	switch (sample_rate) {
		case 8000:
		case 10000:
		case 11025:
		case 16000:
		case 22050:
		case 32000:
		case 44100:
		case 48000:
			thiz->buffer_size = BUFFER_SIZE;
			break;
		case 88200:
		case 96000:
			thiz->buffer_size = BUFFER_SIZE * 2;
			break;
		case 176400:
		case 192000:
			thiz->buffer_size = BUFFER_SIZE * 4;
			break;
		case 352800:
		case 384000:
			thiz->buffer_size = BUFFER_SIZE * 8;
			break;
		default:
			thiz->buffer_size = BUFFER_SIZE * 4;
			break;
	}

	// fill waveformatex
	wf.wFormatTag		= WAVE_FORMAT_PCM;
	wf.nBlockAlign		= sample_size / 8 * channels;
	wf.nAvgBytesPerSec	= sample_rate * wf.nBlockAlign;
	wf.nSamplesPerSec	= sample_rate;
	wf.nChannels		= channels;
	wf.wBitsPerSample	= sample_size;
	wf.cbSize			= sizeof(wf);

	// open sound device
	// WAVE_MAPPER always points to the default wave device on the system
	result = waveOutOpen(&thiz->hWaveOut, WAVE_MAPPER, (WAVEFORMATEX*)&wf,
		(DWORD)waveOutProc, (DWORD)thiz, CALLBACK_FUNCTION);

	if (result != MMSYSERR_NOERROR)
		return 0;

	totalBufferSize = (thiz->buffer_size + sizeof(WAVEHDR)) * BUFFER_COUNT;

	// allocate buffer memory as one big block
	buffer = malloc(totalBufferSize);
	memset(buffer, 0, totalBufferSize);

	// and setup pointers to each buffer
	thiz->waveBlocks = (WAVEHDR*)buffer;
	buffer += sizeof(WAVEHDR) * BUFFER_COUNT;
	for (i = 0; i < BUFFER_COUNT; i++) {
		thiz->waveBlocks[i].dwFlags = WHDR_DONE;
		thiz->waveBlocks[i].lpData  = buffer;
		buffer += thiz->buffer_size;
	}

	thiz->buf_write      = 0;
	thiz->buf_write_pos  = 0;
	thiz->full_buffers   = 0;
	thiz->buffered_bytes = 0;

	return 0;
}

int CWaveOut_Write(CWaveOut* thiz, const short* pcm, int len)
{
	int ret;

	ret = len;

	while (len > 1) {
		int consumed;

		consumed = CWaveOut_WriteNotWait(thiz, pcm, len);

		if (0 == consumed)
			Sleep(1);

		pcm += consumed / 2;
		len -= consumed;
	}

	return ret;
}

int CWaveOut_WriteNotWait(CWaveOut* thiz, const short* pcm, int len)
{
	WAVEHDR* current;
	int len2 = 0;
	int x;
	unsigned char* data = (unsigned char*)pcm;
	unsigned int buf_start_write = thiz->buf_write;

	if (thiz->buffered_bytes == BUFFER_COUNT * thiz->buffer_size) {
		return 0;
	}

	while (len > 0) {
		current = &thiz->waveBlocks[thiz->buf_write];
		if (0 == (current->dwFlags & WHDR_DONE)) {
			// block is full, find next!
			thiz->buf_write = (thiz->buf_write + 1) % BUFFER_COUNT;
			thiz->buf_write_pos = 0;

			if (buf_start_write == thiz->buf_write)
				return len2;

			continue;
		}

		buf_start_write = thiz->buf_write;

		// unprepare the header if it is prepared
		if (current->dwFlags & WHDR_PREPARED)
			waveOutUnprepareHeader(thiz->hWaveOut, current, sizeof(WAVEHDR));

		x = thiz->buffer_size - thiz->buf_write_pos;

		if (x > len) x = len;

		memcpy(current->lpData + thiz->buf_write_pos, data + len2, x);

		len2 += x;
		len  -= x;

		thiz->buffered_bytes += x;
		thiz->buf_write_pos  += x;

		if (thiz->buf_write_pos >= thiz->buffer_size) {
			// prepare header and write data to device
			current->dwBufferLength = thiz->buf_write_pos;

			waveOutPrepareHeader(thiz->hWaveOut, current, sizeof(WAVEHDR));
			waveOutWrite(thiz->hWaveOut, current, sizeof(WAVEHDR));

			// move to the next block
			thiz->buf_write = (thiz->buf_write + 1) % BUFFER_COUNT;
			thiz->buf_write_pos = 0;
		}
	}

	return len2;
}

int CWaveOut_PushSample(CWaveOut* thiz, short sample)
{
	WAVEHDR* current;
	unsigned int buf_start_write = thiz->buf_write;

	if (thiz->buffered_bytes == BUFFER_COUNT * thiz->buffer_size)
		return 0;

	for (;;) {
		current = &thiz->waveBlocks[thiz->buf_write];
		if (current->dwFlags & WHDR_DONE) break;

		// block is full, find next!
		thiz->buf_write = (thiz->buf_write + 1) % BUFFER_COUNT;
		thiz->buf_write_pos = 0;

		if (buf_start_write == thiz->buf_write)
			return 0;
	}

	// unprepare the header if it is prepared
	if (current->dwFlags & WHDR_PREPARED)
		waveOutUnprepareHeader(thiz->hWaveOut, current, sizeof(WAVEHDR));

	*(short*)(current->lpData + thiz->buf_write_pos) = sample;

	thiz->buffered_bytes += 2;
	thiz->buf_write_pos  += 2;

	// prepare header and write data to device
	current->dwBufferLength = thiz->buf_write_pos;

	if (thiz->buf_write_pos >= thiz->buffer_size)
	{
		waveOutPrepareHeader(thiz->hWaveOut, current, sizeof(WAVEHDR));
		waveOutWrite(thiz->hWaveOut, current, sizeof(WAVEHDR));

		// move to the next block
		thiz->buf_write = (thiz->buf_write + 1) % BUFFER_COUNT;
		thiz->buf_write_pos = 0;
	}

	return 1;
}

int CWaveOut_Drain(CWaveOut* thiz)
{
	WAVEHDR* current;

	current = &thiz->waveBlocks[thiz->buf_write];
	if (0 == (current->dwFlags & WHDR_DONE)) {
		// no more data remained
		return 0;
	}

	// prepare header and write data to device
	current->dwBufferLength = thiz->buf_write_pos;

	waveOutPrepareHeader(thiz->hWaveOut, current, sizeof(WAVEHDR));
	waveOutWrite(thiz->hWaveOut, current, sizeof(WAVEHDR));

	thiz->buf_write_pos = 0;

	return 0;
}

int CWaveOut_Close(CWaveOut* thiz)
{
	if (!thiz->hWaveOut)
		return 0;

//	Drain();

	while (thiz->buffered_bytes > 0)
		Sleep(50);

	waveOutPause(thiz->hWaveOut);
	waveOutClose(thiz->hWaveOut);

	free(thiz->waveBlocks);

	thiz->hWaveOut = NULL;

	return 0;
}

void CWaveOut_Pause(CWaveOut* thiz)
{
	waveOutPause(thiz->hWaveOut);
}

void CWaveOut_Resume(CWaveOut* thiz)
{
	waveOutRestart(thiz->hWaveOut);
}
